<?php
/**
 * Control access to application.
 *
 * Remember:
 *   - add ondeny value to handle access denied
 *   - values under access config are evaluated as regular expressions
 *   - $_SESSION['User']->role must be set
 *
 * config.php example:
 *
 * ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 * ; Action to use if access is denied
 * access.ondeny = some_controller.deny_access
 *
 * access.allow.anonymous[] = some_controller.index
 * access.allow.anonymous[] = some_controller.select_music
 * access.deny.anonymous[]  = admin.+
 *
 * access.allow.admin[] = user.delete
 *
 * ; allow everything
 * access.allow.root[] = .*
 *
 * ; conditions
 * access.cond.openForBusiness   = some_controller.sign_in
 * access.cond.closedAfterDark   = .*
 * access.closedAfterDark.ondeny = some_controller.closed_for_the_night
 * ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 *
 */
class Access extends Spaph_Plugin
{
    public static $ACTION = ''; // Remember requested action.
    private $ondeny = '';

    public function __construct()
    {
        // This is the default action to follow ondeny.
        $this->ondeny(Spaph::getInstance()->config()->access->ondeny);
    }

    /**
     * Find if the given role is allowed access to the requested action.
     */
    public function _preDispatch()
    {
        if(!isset($_SESSION['User']->role))
            throw new Exception('The user.role is not set!');
        
        // Remember action.
        self::$ACTION = implode( '.', array_reverse(Spaph::getInstance()->getAction()) );

        // Check any access conditions.
        if( $this->dispatchConditions(self::$ACTION) )
            if( self::isAllowed($_SESSION['User']->role, self::$ACTION ) )
                return;

        // Access has been denied. So, follow the ondeny action.
        $do = explode('.',$this->ondeny());
        Spaph::getInstance()->setAction($do[1],$do[0]);
    }

    /**
     * Can the role access an action?
     *
     * @param string $role
     * @param string $action
     * @return boolean
     */
    public static function isAllowed($role, $action)
    {
        if($mapped = Spaph::getInstance()->map($action))
            $action = $mapped;

        if($deny = Spaph::getInstance()->config()->access->deny) {
            if($deny->$role) {
                foreach($deny->$role->toArray() as $regex) {
                    if(preg_match('/^'.$regex.'$/i',$action)) {
                        return false;
                    }
                }
            }
        }

        if($allow = Spaph::getInstance()->config()->access->allow) {
            if($allow->$role) {
                foreach($allow->$role->toArray() as $regex) {
                    if(preg_match('/^'.$regex.'$/i',$action)) {
                        return true;
                    }
                }
            }
        }

        // By default, deny access.
        return false;
    }

    /**
     * This can be called by conditions.
     *
     * @param string $action
     * @return string
     */
    private function ondeny($action=null)
    {
        if(isset($action))
            $this->ondeny = $action;

        return $this->ondeny;
    }

    /**
     * Dispatch static methods known as conditions. They are defined in
     * Spaph_Config like access.cond.MyConditionStaticMethod = some.action
     *
     * By default, the action.ondeny will be used. However, another action
     * can be used for ondeny by defining something like:
     *
     * access.MyConditionStaticMethod.ondeny = some.denyaction
     *
     * @param string $action
     * @return boolean
     */
    private function dispatchConditions($action)
    {
        if($conditions = Spaph::getInstance()->config()->access->cond) {
            foreach($conditions->toArray() as $func => $regex) {
                if(preg_match('/^'.$regex.'$/i',$action)) {
                    if(self::$func())
                        continue;

                    if(Spaph::getInstance()->config()->access->$func->ondeny)
                        $this->ondeny(Spaph::getInstance()->config()->access->$func->ondeny);

                    return false;
                }
            }
        }

        return true;
    }

    /****************************************************************
      Conditions - These static methods must return true or false.
                   ondeny can be set if appropriate. Otherwise, the
                   value for the config setting access.ondeny will
                   be used. These conditions will be called if they
                   are configured to do so in the config file. For
                   example,

                       access.cond.myCondition = main.sign_in

                   that will trigger Access::myCondition() to be
                   called durint Access::_preDispatch().
     ****************************************************************/

    /**
     * Are we open for business
     *
     * @return boolean
     */
    public static function openForBusiness()
    {
        return true;
    }

    /**
     * Taking care of maintenance.
     *
     * @return boolean
     */
    public static function maintenance()
    {
        return true;
    }
}
